using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Globalization;
using System.Xml;

namespace Nuane.Posterous
{

	/// <summary>
	/// Posterous session based on credentials or hostname.
	/// </summary>
	public class PosterousSession
	{
		//Error response:
		//  <rsp stat="fail">
		//    <err code="3001" msg="Invalid Posterous email or password" />
		//  </rsp>
		// * 1003 - Invalid media
		// * 2001 - Invalid url. Post not found
        // * 2002 - Post is private
		// * 3001 - Invalid Posterous email or password
		// * 3002 - Invalid site id
		// * 3003 - User does not have access to this site
		// * 3004 - Unable to save post
		// * 3005 - Unable to get posts
		// * 3006 - Invalid date
		// * 3007 - Invalid post id. Post not found
		// * 3008 - Comments disabled on this site
		// * 3009 - Unable to save comment

		/// <summary>
		/// Occurs when a chunk of data was uploaded to posterous.
		/// </summary>
		public event EventHandler<PosterousUploadProgressEventArgs> UploadProgress;

		/// <summary>
		/// Timeout value (in miliseconds) used while waiting for server response.
		/// </summary>
		/// <value>Timeout value.</value>
		public int Timeout { get; set; }

		/// <summary>
		/// Upload block size. Default is 16384 (16KB).
		/// </summary>
		/// <value>Upload block size.</value>
		public int UploadBlockSize { get; set; }

		/// <summary>
		/// Gets or sets the name of your application or website.
		/// </summary>
		/// <value>Name of your application or website.</value>
		public string ApplicationName { get; set; }

		/// <summary>
		/// Gets or sets the link to your application or website.
		/// </summary>
		/// <value>Link to your application or website</value>
		public Uri ApplicationUri { get; set; }

		private readonly string _token;
		private string _hostname;
		private int _siteId;
		private bool _hostnameResolved;

		private PosterousSession()
		{
			UploadBlockSize = 16 * 1024;
			Timeout = 120 * 1000;
		}

		/// <summary>
		/// Creates a posterous session based on the specified credentials.
		/// </summary>
		/// <param name="email"></param>
		/// <param name="password"></param>
		public PosterousSession(string email, string password)
			: this()
		{
			if (email == null)
				throw new ArgumentNullException("email");

			if (password == null)
				throw new ArgumentNullException("password");

			string credentials = string.Format(CultureInfo.InvariantCulture, "{0}:{1}", email, password);
			_token = Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials));
		}

		/// <summary>
		/// Creates a posterous session based on the specified hostname.
		/// </summary>
		/// <param name="hostname">Posterous hostname. Custom domains are not acceptable.</param>
		public PosterousSession(string hostname)
			: this()
		{
			if (hostname == null)
				throw new ArgumentNullException("hostname");

			// remove .posterous.com if present
			hostname = hostname.ToLowerInvariant();
			string ending = ".posterous.com";
			if (hostname.EndsWith(ending))
				hostname = hostname.Substring(0, hostname.Length - ending.Length);

			_hostname = hostname;
		}

		/// <summary>
		/// Creates a posterous session based on the specified site ID.
		/// </summary>
		/// <param name="siteId">Posterous hostname. Custom domains are not acceptable.</param>
		public PosterousSession(int siteId)
			: this()
		{
			if (siteId <= 0)
				throw new ArgumentOutOfRangeException("siteId", "Invalid site ID.");

			_siteId = siteId;
		}

		private class InvokeResult
		{
			public XmlDocument Xml { get; private set; }
			public int RunTime { get; private set; }

			public InvokeResult(HttpWebResponse response)
			{
				Stream stream = response.GetResponseStream();
				StreamReader reader = new StreamReader(stream);
				string content = reader.ReadToEnd();
				XmlDocument xml = new XmlDocument();
				xml.LoadXml(content);

				XmlElement rsp = xml.DocumentElement;
				if (rsp.Name != "rsp")
					throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);

				XmlAttribute stat = rsp.Attributes["stat"];
				if (stat == null)
					throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);

				switch (stat.Value)
				{
					case "ok":
						break;
					case "fail":
						XmlElement err = rsp["err"];
						if (err == null)
							throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);
						int code = Helper.ToInt32(Helper.GetAttributeValue(err, "code"));
						string message = string.Format("{1} ({0}).", code, Helper.GetAttributeValue(err, "msg").TrimEnd('.'));
						throw new PosterousException(message, PosterousExceptionStatus.ProtocolError, code);
					default:
						throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);
				}

				Xml = xml;

				string rt = response.Headers.Get("X-Runtime");
				if (rt != null && rt.EndsWith("ms"))
				{
					rt = rt.Substring(0, rt.Length - 2);
					RunTime = Helper.ToInt32(rt);
				}
				else
				{
					RunTime = 0;
				}
			}
		}

		private InvokeResult Invoke(string uri, IPosterousData postData, string token)
		{
			string splitter;
			byte[] data;

			if (postData != null)
			{
				splitter = Guid.NewGuid().ToString();
				string urlParameters;
				data = postData.ToData(splitter, ApplicationName, ApplicationUri, out urlParameters);
				if (uri.IndexOf('?') >= 0)
					uri += urlParameters;
				else
					uri += "?" + urlParameters.Substring(1);
			}
			else
			{
				splitter = null;
				data = null;
			}

			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
			request.KeepAlive = true;
			if (token != null)
				request.Headers.Add(string.Format("Authorization: Basic {0}", token));

			if (data != null)
			{
				int uploadBlockSize = Math.Max(256, UploadBlockSize);
				request.Method = "POST";
				request.ContentType = string.Format("multipart/form-data; boundary={0}", splitter);
				request.ContentLength = data.Length;
				request.Timeout = -1;
				Stream rs = request.GetRequestStream();
				for (int i = 0; i < data.Length; i += uploadBlockSize)
				{
					int len = Math.Min(uploadBlockSize, data.Length - i);
					rs.Write(data, i, len);
					rs.Flush();
					if (UploadProgress != null)
						UploadProgress(this, new PosterousUploadProgressEventArgs(i + len, data.Length));
				}
				rs.Close();
			}

			HttpWebResponse response;
			try
			{
				request.Timeout = Math.Max(-1, Timeout);
				response = (HttpWebResponse)request.GetResponse();
			}
			catch (WebException x)
			{
				if (x.Status != WebExceptionStatus.ProtocolError)
				{
					throw new PosterousException("Error while communicating with the server.", PosterousExceptionStatus.HttpError, x);
				}
				response = (HttpWebResponse)x.Response;
			}

			try
			{
				return new InvokeResult(response);
			}
			finally
			{
				response.Close();
			}
		}

		private void ResolveHostname()
		{
			if (_hostname == null || _hostnameResolved)
				return;

			if (_hostname.IndexOf(".") < 0)
			{
				_hostnameResolved = true;
				return;
			}

			string uri = "http://" + _hostname + "/?page=999999999&c=1";

			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
			request.KeepAlive = false;

			HttpWebResponse response;
			try
			{
				request.Timeout = Math.Max(-1, Timeout);
				response = (HttpWebResponse)request.GetResponse();
			}
			catch (WebException x)
			{
				if (x.Status != WebExceptionStatus.ProtocolError)
				{
					throw new PosterousException("Error while communicating with the server.", PosterousExceptionStatus.HttpError, x);
				}
				response = (HttpWebResponse)x.Response;
			}

			try
			{
				if (response.ContentLength > 64 * 1024)
					throw new PosterousException("Unable to resolve custom domain, response is too long.", PosterousExceptionStatus.OperationFailure);

				if (response.ContentLength < 1)
					throw new PosterousException("Unable to resolve custom domain, response is too short.", PosterousExceptionStatus.OperationFailure);

				string html;
				using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.ASCII))
				{
					html = reader.ReadToEnd();
				}

				string header = "/posts/addsub/?site=";
				int s = html.IndexOf(header);
				if (s < 0)
					throw new PosterousException("Unable to resolve custom domain, subscribe link not found.", PosterousExceptionStatus.OperationFailure);

				s += header.Length;
				int e = html.IndexOf("&amp", s);
				if (e < 0)
					throw new PosterousException("Unable to resolve custom domain, subscribe link is not understood.", PosterousExceptionStatus.OperationFailure);

				string siteId = html.Substring(s, e - s);

				int.TryParse(siteId, NumberStyles.None, CultureInfo.InvariantCulture, out _siteId);
				if (_siteId <= 0)
				{
					_siteId = 0;
					throw new PosterousException("Unable to resolve custom domain, unexpected site ID.", PosterousExceptionStatus.OperationFailure);
				}

				_hostnameResolved = true;
			}
			finally
			{
				response.Close();
			}
		}

		/// <summary>
		/// Gets the list of sites (blogs) for the specified account. Only works if credentials were specified.
		/// </summary>
		/// <returns>List of sites.</returns>
		public PosterousSiteCollection GetSites()
		{
			//URL
			//    * http://posterous.com/api/getsites
			//Fields
			//    * None
			//Response
			//  <rsp stat="ok">  
			//    <site>  
			//      ...  
			//    </site>  
			//  </rsp> 
			//
			//The response includes a list of all sites owned and authored by this user.

			if (_token == null)
				throw new InvalidOperationException("Authentication required.");

			string uri = "http://posterous.com/api/getsites";

			InvokeResult result = Invoke(uri, null, _token);
			XmlDocument xml = result.Xml;

			PosterousSiteCollection list = new PosterousSiteCollection();

			XmlNode node = xml["rsp"].FirstChild;
			while (node != null)
			{
				list.Add(new PosterousSite(this, node));
				node = node.NextSibling;
			}

			return list;
		}

		/// <summary>
		/// Connects to the specified posterous site or account and verifies that the user credentials work.
		/// </summary>
		/// <returns>Time spend by posterous serving the request.</returns>
		public int Ping()
		{
			// let's simply read a non-existent post here

			ResolveHostname();

			StringBuilder uri = new StringBuilder("http://posterous.com/api/readposts");
			uri.AppendFormat("?num_posts={0}&page={1}", 1, int.MaxValue);
			if (_hostname != null)
				uri.AppendFormat("&hostname={0}", _hostname);

			InvokeResult result = Invoke(uri.ToString(), null, _token);
			return result.RunTime;
		}

		/// <summary>
		/// Gets posterous post with the specified Post.ly shortcode.
		/// </summary>
		/// <param name="shortCode">Post.ly shortcode (the ABCD part of http://post.ly/ABCD).</param>
		/// <returns>Posterous post.</returns>
		public PosterousPost GetPost(string shortCode)
		{
			if (shortCode == null)
				throw new ArgumentNullException("shortCode");

			//URL
			//  * http://posterous.com/api/getpost
			//Fields
			//  * "id" - Required. Post.ly shortcode. (Example: 123abc in http://post.ly/123abc)

			string uri = "http://posterous.com/api/getpost";
			uri += "?id=" + shortCode;

			InvokeResult result = Invoke(uri, null, null);
			XmlDocument xml = result.Xml;

			XmlNode node = xml["rsp"].FirstChild;
			if (node == null)
				throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);
			PosterousPost post = new PosterousPost(this, node, true);
			return post;
		}

		/// <summary>
		/// Gets posterous post with the specified ID.
		/// </summary>
		/// <param name="postId">Post ID.</param>
		/// <returns>Posterous post.</returns>
		public PosterousPost GetPost(int postId)
		{
			//URL
			//  * http://posterous.com/api/getpost
			//Fields
			//  * "post_id" - Undocumented

			string uri = "http://posterous.com/api/getpost";
			uri += "?post_id=" + postId;

			InvokeResult result = Invoke(uri, null, null);
			XmlDocument xml = result.Xml;

			XmlNode node = xml["rsp"].FirstChild;
			if (node == null)
				throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);
			PosterousPost post = new PosterousPost(this, node, true);
			return post;
		}

		/// <summary>
		/// Publishes the specified post.
		/// </summary>
		/// <param name="siteId">Site ID. If 0, primary site is used.</param>
		/// <param name="newPost">New post.</param>
		/// <returns>The new post info.</returns>
		public PosterousPostInfo PublishPost(int siteId, PosterousNewPost newPost)
		{
			if (newPost == null)
				throw new ArgumentNullException("newPost");

			if (_token == null)
				throw new InvalidOperationException("Authentication required.");

			string uri = "http://posterous.com/api/newpost";

			if (siteId != 0)
				uri += "?site_id=" + siteId;

			InvokeResult result = Invoke(uri, newPost, _token);
			XmlDocument xml = result.Xml;

			XmlNode node = xml["rsp"].FirstChild;
			if (node == null)
				throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);
			PosterousPostInfo postInfo = new PosterousPostInfo(this, node);
			return postInfo;
		}


		/// <summary>
		/// Publishes the specified post.
		/// </summary>
		/// <param name="siteId">Site ID. If 0, primary site is used.</param>
		/// <param name="title">Title. Cannot be null.</param>
		/// <param name="body">Body. May be null.</param>
		/// <returns>The new post (only partially initialized if private).</returns>
		public PosterousPostInfo PublishPost(int siteId, string title, string body)
		{
			if (title == null)
				throw new ArgumentNullException("title");

			if (_token == null)
				throw new InvalidOperationException("Authentication required.");

			PosterousNewPost post = new PosterousNewPost();
			post.Title = title;
			post.Body = body;
			return PublishPost(siteId, post);
		}

		internal PosterousPostInfo UpdatePost(int postId, PosterousNewPost newPost)
		{
			if (_token == null)
				throw new InvalidOperationException("Authentication required.");

			string uri = "http://posterous.com/api/updatepost";
			uri += "?post_id=" + postId;

			InvokeResult result = Invoke(uri, newPost, _token);
			XmlDocument xml = result.Xml;

			XmlNode node = xml["rsp"].FirstChild;
			if (node == null)
				throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);
			PosterousPostInfo postInfo = new PosterousPostInfo(this, node);
			return postInfo;
		}

		/// <summary>
		/// Updates the post's body and title.
		/// </summary>
		/// <param name="postId">Post ID.</param>
		/// <param name="body">Post body (no update if null).</param>
		/// <param name="title">Post title (no update if null).</param>
		/// <returns>The post info.</returns>
		public PosterousPostInfo UpdatePost(int postId, string title, string body)
		{
			if (_token == null)
				throw new InvalidOperationException("Authentication required.");

			PosterousNewPost post = new PosterousNewPost();
			post.Body = body;
			post.Title = title;

			return UpdatePost(postId, post);
		}

		/// <summary>
		/// Adds additional media to the specified post.
		/// </summary>
		/// <param name="postId">Post ID.</param>
		/// <param name="media">One or more media files.</param>
		/// <returns>The post info.</returns>
		public PosterousPostInfo AddMedia(int postId, params PosterousFile[] media)
		{
			if (_token == null)
				throw new InvalidOperationException("Authentication required.");

			PosterousNewPost post = new PosterousNewPost();
			if (media != null)
				post.Files.AddRange(media);

			return UpdatePost(postId, post);			
		}

		/// <summary>
		/// Adds a a new comment to the specified post.
		/// </summary>
		/// <param name="postId">Post ID.</param>
		/// <param name="comment">Comment.</param>
		/// <returns>The post info.</returns>
		public PosterousPostInfo AddComment(int postId, PosterousNewComment comment)
		{
			if (comment == null)
				throw new ArgumentNullException("comment");

			if (_token == null)
				throw new InvalidOperationException("Authentication required.");

			//URL
			//    * http://posterous.com/api/newcomment
			//Fields
			//    * "post_id" - The post id to comment on
			//    * "comment" - The comment body
			//    * "name" - Optional. The name to use
			//    * "email" - Optional. The email address to use
			//    * "date" - Optional. In GMT. Any parsable format. Cannot be in the future.
			//Response
			//  <rsp stat="ok">  
			//    <comment>  
			//      <id>24766</id>  
			//    </comment>  
			//    <post>  
			//      <id>111455</id>  
			//      <url>http://post.ly/3C</url>  
			//      <longurl>http://sachin.posterous.com/hello-world</longurl>  
			//      <title>Breakfast in sf</title>  
			//    </post>  
			//  </rsp>

			string uri = "http://posterous.com/api/newcomment";
			uri += "?post_id=" + postId;

			InvokeResult result = Invoke(uri, comment, _token);
			XmlDocument xml = result.Xml;

			XmlNode node = xml["rsp"].FirstChild;
			if (node == null || node.Name != "comment")
				throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);
			Helper.ToInt32(Helper.GetChildElementValue(node, "id"));

			node = node.NextSibling;
			if (node == null)
				throw new PosterousException("Invalid response.", PosterousExceptionStatus.ServerProtocolViolation);
			PosterousPostInfo postInfo = new PosterousPostInfo(this, node);
			return postInfo;
		}

		private int GetPosts(List<PosterousPost> posts, Dictionary<int, object> skip, int siteId, int postCount, int pageNumber, string tag)
		{
			//URL
			//    * http://posterous.com/api/readposts
			//Fields
			//    * "site_id" - Optional. Id of the site to read from
			//    * "hostname" - Optional. Subdomain of the site to read from
			//    * "num_posts" - Optional. How many posts you want. Default is 10, max is 50
			//    * "page" - Optional. What 'page' you want (based on num_posts). Default is 1
			//    * "tag" - Optional
			//Response
			//  <rsp stat="ok">
			//    <post>
			//    ...
			//    </post>
			//  </rsp>
			//
			//This call can be authenticated or not. If it is not authenticated, either the site_id or hostname is required, and it will only return public posts.

			ResolveHostname();

			StringBuilder uri = new StringBuilder("http://posterous.com/api/readposts");
			uri.AppendFormat("?num_posts={0}&page={1}", postCount, pageNumber);

			if (siteId != 0)
				uri.AppendFormat("&site_id={0}", siteId);
			else if (_siteId != 0)
				uri.AppendFormat("&site_id={0}", _siteId);
			else if (_hostname != null)
				uri.AppendFormat("&hostname={0}", _hostname);

			if (tag != null)
				uri.AppendFormat("&tag={0}", tag);

			InvokeResult result = Invoke(uri.ToString(), null, _token);
			XmlDocument xml = result.Xml;

			int count = 0;
			XmlNode node = xml["rsp"].FirstChild;
			while (node != null)
			{
				count++;
				PosterousPost post = new PosterousPost(this, node, false);
				int id = post.Id;
				if (!skip.ContainsKey(id))
				{
					posts.Add(post);
					skip.Add(id, null);
				}
				node = node.NextSibling;
			}

			return count;
		}

		// Magic table :-)
		private static int[] lengths = {50, 40, 30, 20, 15, 10, 5, 4, 3, 2, 1};

		// Magic section :-)
		private struct Section
		{
			public readonly int FirstPage;
			public readonly int PostsPerPage;
			public readonly int Pages;

			public Section(int offset, int size, int count)
			{
				PostsPerPage = size;
				FirstPage = 1 + offset / size;
				Pages = count;
			}
		}

		// Do some experimental magic :-)
		private static void Split(int start, int end, List<Section> list)
		{
			if (start >= end)
				return;

			for (int j=0; j<lengths.Length; j++)
			{
				int size = lengths[j];
				
				int s = ((start + (size - 1)) / size) * size;
				if (s >= start && (s + size) <= end)
				{
					Split(start, s, list);
					int n = (end - s) / size;
					list.Add(new Section(s, size, n));
					s += size * n;

					Split(s, end, list);
					return;
				}
			}
		}

		/// <summary>
		/// Get posts from the specified site.
		/// </summary>
		/// <param name="siteId">Site ID. If 0, the primary site is used.</param>
		/// <param name="offset">Offset of the first post (zero-based).</param>
		/// <param name="count">Number of posts to retrieve.</param>
		/// <param name="tag">Tag.</param>
		/// <returns>Collection of posts.</returns>
		public PosterousPostCollection GetPosts(int siteId, int offset, int count, string tag)
		{
			List<Section> list = new List<Section>();
			int end = Math.Min(2 * 1000 * 1000 * 1000, offset + count);
			Split(offset, end, list);
			
			PosterousPostCollection posts = new PosterousPostCollection();
			Dictionary<int, object> skip = new Dictionary<int, object>();

			for (int i = 0; i < list.Count; i++)
			{
				int postsPerPage = list[i].PostsPerPage;
				int pageNumber = list[i].FirstPage;
				int pages = list[i].Pages;
				for (int j = 0; j < pages; j++)
				{
					int n = GetPosts(posts, skip, siteId, postsPerPage, pageNumber, tag);
					if (n == 0)
						return posts;
					pageNumber++;
				}
			}

			return posts;
		}
		
		/// <summary>
		/// Get posts from the specified site.
		/// </summary>
		/// <param name="siteId">Site ID. If 0, the primary site is used.</param>
		/// <param name="offset">Offset of the first post (zero-based).</param>
		/// <param name="count">Number of posts to retrieve.</param>
		/// <returns>Collection of posts.</returns>
		public PosterousPostCollection GetPosts(int siteId, int offset, int count)
		{
			return GetPosts(siteId, offset, count, null);
		}

	}

}
